home *** CD-ROM | disk | FTP | other *** search
/ Language/OS - Multiplatform Resource Library / LANGUAGE OS.iso / smaltalk / st80_r41.lha / st80_r41 / NewsReader.st < prev    next >
Text File  |  1993-07-23  |  51KB  |  1,900 lines

  1. Object subclass: #NewsGroup
  2.     instanceVariableNames: 'name readMessages subjects '
  3.     classVariableNames: 'SaveDirectory SaveFormat '
  4.     poolDictionaries: ''
  5.     category: 'Tools-NewsReader'!
  6. NewsGroup comment:
  7. 'An instance of Newsgroup represents a newsgroup, plus a list of read messages in that newsgroup.
  8.  
  9. Instance variables:
  10.     name            <String>
  11.                         Of the form area.chapter.section.subsection
  12.     readMessages    <?>
  13.                         List of message numbers marked as "read"
  14.     subjects        <Array of Subject>
  15.                         This is used to cache current subjects during news reading.
  16.  
  17. Class variables:
  18.     SaveDirectory    <String>
  19.                         A string representing the directory where news messages
  20.                         are to be saved.  It should end with a delimiter.  For
  21.                         example: ''/users/myhome/News/''
  22.     SaveFormat        <#mail | #news>
  23.                         If #mail, messages will be appended to a unique newsgroup
  24.                         file in SaveDirectory.  If #news, messages will be saved
  25.                         individually in a unique subdirectory under SaveDirectory
  26.                         corresponding to the NewsGroup.'!
  27.  
  28.  
  29. !NewsGroup methodsFor: 'initialization'!
  30.  
  31. initializeFromRnString: aString
  32.  
  33.     "Initialize this instance based on the .newsrc format supplied in aString"
  34.  
  35.     | sepIdx |
  36.     sepIdx := aString indexOf: $:.
  37.     sepIdx = 0 ifTrue: [ ^self error: 'Unknown format' ].
  38.     name := aString copyFrom: 1 to: sepIdx - 1.
  39.     readMessages := SparseIntegerCollection fromRnString: (aString copyFrom: sepIdx + 1 to: aString size).
  40.     subjects := nil.!
  41.  
  42. initializeName: aString
  43.  
  44.     "Initialize this instance, setting name to aString"
  45.  
  46.     name := aString.
  47.     readMessages := SparseIntegerCollection new.
  48.     subjects := nil.! !
  49.  
  50. !NewsGroup methodsFor: 'comparing'!
  51.  
  52. = aNewsgroup
  53.  
  54.     (aNewsgroup isMemberOf: self class)
  55.         ifTrue: [ ^name = aNewsgroup name ]
  56.         ifFalse: [ ^false ]! !
  57.  
  58. !NewsGroup methodsFor: 'subjects'!
  59.  
  60. addSubject: aSubject
  61.  
  62.     subjects isNil ifTrue: [ subjects := OrderedCollection new ].
  63.     subjects add: aSubject.
  64.     aSubject setGroup: self.
  65.     self markSubjectAsUnread: aSubject!
  66.  
  67. allSubjectsReadOrUnread
  68.  
  69.     "Return an array of all NewsSubjects currently on disk"
  70.  
  71.     ^(self class named: name) refreshSubjects!
  72.  
  73. emptyCaches
  74.  
  75.     "Empty cached subject list"
  76.  
  77.     subjects := nil!
  78.  
  79. newOrExistingSubjectNamed: subjectString
  80.  
  81.     "This returns a NewsSubject representing the subject named subjectString.  If I already know of such a subject, this returns that NewsSubject.  Otherwise, a new one is created, added to myself, and returned"
  82.  
  83.     | newSubject |
  84.     newSubject := NewsSubject newWithString: subjectString inGroup: self.
  85.     ^subjects
  86.         detect: [ :aSub | aSub = newSubject ]
  87.         ifNone: [ self addSubject: newSubject. newSubject ]!
  88.  
  89. nonEmptySubjects
  90.  
  91.     "Clean up the cache -- get rid of subjects that have no unread messages.  Return the list."
  92.  
  93.     subjects copy do: [ :aSubject |
  94.         aSubject removeReadMessages messages isEmpty ifTrue: [ subjects remove: aSubject ]].
  95.  
  96.     ^subjects!
  97.  
  98. refreshedSubjects
  99.  
  100.     "Returns an OrderedCollection of NewsSubjects"
  101.  
  102.     ^subjects isNil
  103.         ifTrue: [ self refreshSubjects ]
  104.         ifFalse: [ self nonEmptySubjects ]!
  105.  
  106. refreshSubjects
  107.  
  108.     "Empty cached subject list, and refresh by scanning my directory.  Show wait cursor, as it might take awhile."
  109.  
  110.     | dirContents |
  111.     subjects := OrderedCollection new.
  112.  
  113.     Cursor wait showWhile: [
  114.         dirContents := self messageDirectoryContents.
  115.  
  116.         subjects := OrderedCollection new.
  117.         dirContents do: [:each |
  118.             (self haveRead: each) ifFalse: [ NewsMessage named: each cacheInGroup: self ]]].
  119.  
  120.     ^subjects!
  121.  
  122. reviveSubject: aNewsSubject
  123.  
  124.     "Scan my directory for all messages with aNewsSubject, and mark those messages as unread.  Show wait cursor, as it might take awhile."
  125.  
  126.     | dirContents |
  127.  
  128.     Cursor wait showWhile: [
  129.         dirContents := self messageDirectoryContents.
  130.  
  131.         dirContents do: [:each |
  132.             NewsMessage named: each ifInSubject: aNewsSubject cacheInGroup: self ].
  133.  
  134.         self markSubjectAsUnread: aNewsSubject ]!
  135.  
  136. subjects
  137.  
  138.     ^subjects! !
  139.  
  140. !NewsGroup methodsFor: 'marking messages read'!
  141.  
  142. catchUp
  143.  
  144.     "Mark all existing messages as read"
  145.  
  146.     "Note: don't iterate over subjects instvar, as this is only a cache, and could be out of sync"
  147.     "self messageDirectoryContents do: [ :aNewsMessage |
  148.         readMessages add: aNewsMessage asNumber ]."
  149.  
  150.     "But I only want to mark the cache as read!!"
  151.     subjects do: [ :subj | subj messages do: [ :msg | readMessages add: msg asNumber ]].
  152.  
  153.     readMessages collapse.
  154.     subjects := OrderedCollection new.!
  155.  
  156. markAsRead: aMessageOrMessageNumber
  157.  
  158.     "Add aMessageOrMessageNumber to the list of read messages"
  159.  
  160.     readMessages add: aMessageOrMessageNumber asInteger!
  161.  
  162. markAsUnread: aMessageOrMessageNumber
  163.  
  164.     "Remove aMessageOrMessageNumber from the list of read messages"
  165.  
  166.     readMessages remove: aMessageOrMessageNumber asInteger ifAbsent: [ nil ]!
  167.  
  168. markSubjectAsRead: aNewsSubject
  169.  
  170.     "Add all messages in aNewsSubject to the list of read messages"
  171.  
  172.     aNewsSubject messages do: [ :aNewsMessage |
  173.         readMessages add: aNewsMessage asInteger ].
  174.     subjects remove: aNewsSubject ifAbsent: [ nil ].!
  175.  
  176. markSubjectAsUnread: aNewsSubject
  177.  
  178.     "Remove all messages in aNewsSubject from the list of read messages"
  179.  
  180.     aNewsSubject messages do: [ :aNewsMessage |
  181.         readMessages
  182.             remove: aNewsMessage asInteger
  183.             ifAbsent: [ nil ]].!
  184.  
  185. unreadAll
  186.  
  187.     "Empty the readMessages collection - this causes all available messages in this group to be available to the news reader."
  188.  
  189.     readMessages := SparseIntegerCollection new.
  190.     self refreshSubjects! !
  191.  
  192. !NewsGroup methodsFor: 'saving messages'!
  193.  
  194. asFileName
  195.  
  196.     "Can make one of two formats:
  197.         .../<area>/<chapter>[/<section>[/<subsection>]]/<number>
  198.         where each message is an individual file in its newsgroup directory
  199.     or
  200.         .../Area.chapter.section.subsection
  201.         where a message is appended to its newsgroup file"
  202.  
  203.     SaveFormat = #mail ifTrue: [
  204.         ^Filename named: SaveDirectory, self printString ].
  205.     SaveFormat = #news ifTrue: [
  206.         ].
  207.     ^self error: 'Unrecognized save format'!
  208.  
  209. saveMailFormat: aMessage
  210.  
  211.     "Append aMessage in mail format to this NewsGroup's save file"
  212.  
  213.     | fn ws requestor |
  214.  
  215.     requestor := ('*4.0*' match: Smalltalk version)
  216.         ifTrue: [ Filename ]
  217.         ifFalse: [ DialogView ].
  218.  
  219.     fn := requestor
  220.         requestFileName: 'File to append to'
  221.         default: SaveDirectory, self printString
  222.         version: #any
  223.         ifFail: [ ^nil ].
  224.     ws := (Filename named: fn) appendStream.
  225.     [ ws nextPutAll: (self mailTextForMessage: aMessage) ] valueNowOrOnUnwindDo: [ ws close ]!
  226.  
  227. saveMessage: aMessage
  228.  
  229.     | format |
  230.     format := SaveFormat.
  231.     InputState default shiftDown ifTrue: [
  232.         SaveFormat := DialogView
  233.             choose: 'Format?'
  234.             labels: #('mail' 'news')
  235.             values: #(#mail #news)
  236.             default: SaveFormat].
  237.  
  238.     format = #mail ifTrue: [
  239.         ^self saveMailFormat: aMessage ].
  240.     format = #news ifTrue: [
  241.         ^self saveNewsFormat: aMessage
  242.         ].
  243.     ^self error: 'Unrecognized save format'!
  244.  
  245. saveNewsFormat: aMessage
  246.  
  247.     "Save aMessage in this NewsGroup's save directory"
  248.  
  249.     | fn ws |
  250.     fn := (Filename
  251.         request: 'Save to'
  252.         initially: SaveDirectory, self newsDirectory, aMessage nameOfFile
  253.         shouldExist: false).
  254.     fn directory exists ifFalse: [ fn directory makeDirectory ].
  255.     ws := fn writeStream.
  256.     [ ws nextPutAll: (self textForMessage: aMessage) ] valueNowOrOnUnwindDo: [ ws close ]! !
  257.  
  258. !NewsGroup methodsFor: 'message text'!
  259.  
  260. mailTextForMessage: aMessage
  261.  
  262.     "Answer the text for aMessage in this group, in mail format."
  263.  
  264.     | msgText msgStream mailStream |
  265.     msgText := self textForMessage: aMessage.
  266.     msgStream := msgText readStream.
  267.     mailStream := String new writeStream.
  268.  
  269.     msgStream skipToAll: 'From: '; skipSeparators.
  270.     msgStream skip: 6.
  271.     mailStream nextPutAll: 'From '; nextPutAll: (msgStream upTo: Character cr); cr.
  272.     msgStream reset.
  273.     msgStream skipToAll: 'Date: '; skipSeparators.
  274.     mailStream nextPutAll: (msgStream upTo: Character cr); cr.
  275.     msgStream reset.
  276.     msgStream skipToAll: 'From: '; skipSeparators.
  277.     mailStream nextPutAll: (msgStream upTo: Character cr); cr.
  278.     msgStream reset.
  279.     msgStream skipToAll: 'Subject: '; skipSeparators.
  280.     mailStream nextPutAll: (msgStream upTo: Character cr); cr.
  281.     msgStream skipToAll: (String with: Character cr with: Character cr).
  282.     mailStream nextPutAll: (msgStream "upToEnd (too slow)" next: (msgText size - msgStream position)).
  283.     mailStream cr; cr.
  284.  
  285.     ^mailStream contents!
  286.  
  287. textForMessage: aMessage
  288.  
  289.     "Answer the text for aMessage in this group."
  290.  
  291.     ^(self messageDirectory construct: aMessage nameOfFile) contentsOfEntireFile asText! !
  292.  
  293. !NewsGroup methodsFor: 'name'!
  294.  
  295. name
  296.  
  297.     ^name! !
  298.  
  299. !NewsGroup methodsFor: 'testing'!
  300.  
  301. haveRead: aMessage
  302.  
  303.     "Answer true if aMessage has been read.  The argument can be a NewsMessage, a number, or a String"
  304.  
  305.     ^readMessages includes: aMessage asNumber! !
  306.  
  307. !NewsGroup methodsFor: 'printing'!
  308.  
  309. printString
  310.  
  311.     ^name!
  312.  
  313. storeRnStyleOn: aStream
  314.  
  315.     "Store this like it would look in an rn setup file, .newsrc"
  316.  
  317.     aStream nextPutAll: self printString; nextPut: $:; space.
  318.     readMessages storeRnStyleOn: aStream! !
  319.  
  320. !NewsGroup methodsFor: 'newsgroup directory'!
  321.  
  322. messageDirectory
  323.  
  324.     "Answer the directory where my messages are found."
  325.  
  326.     | dir part tname |
  327.  
  328.     tname := name copy.
  329.     dir := NewsReader newsDirectory.
  330.     [part := tname copyUpTo: $..
  331.      dir := dir construct: part.
  332.      part = tname ifTrue: [ tname := '' ] ifFalse: [
  333.         tname := tname copyFrom: part size + 2 to: tname size].
  334.      tname isEmpty ] whileFalse.
  335.     dir isReadable
  336.         ifTrue: [^dir]
  337.         ifFalse: [^self error: dir, ' unreadable']!
  338.  
  339. messageDirectoryContents
  340.  
  341.     | dir |
  342.     dir := self messageDirectory.
  343.     ^dir directoryContents asSortedCollection! !
  344. "-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!
  345.  
  346. NewsGroup class
  347.     instanceVariableNames: ''!
  348.  
  349.  
  350. !NewsGroup class methodsFor: 'class initialization'!
  351.  
  352. initialize
  353.  
  354.     "Initialize class variables"
  355.     "NewsGroup initialize"
  356.  
  357.     SaveDirectory := '$HOME/News/'.
  358.     SaveFormat := #mail! !
  359.  
  360. !NewsGroup class methodsFor: 'instance creation'!
  361.  
  362. fromRnString: aString
  363.  
  364.     "Create an instance based on the .newsrc string, aString"
  365.  
  366.     ^self new initializeFromRnString: aString!
  367.  
  368. named: aString
  369.  
  370.     ^self new initializeName: aString! !
  371.  
  372. Object subclass: #NewsMessage
  373.     instanceVariableNames: 'printString number '
  374.     classVariableNames: ''
  375.     poolDictionaries: ''
  376.     category: 'Tools-NewsReader'!
  377.  
  378.  
  379. !NewsMessage methodsFor: 'initialization'!
  380.  
  381. initializeNumber: aNumber group: aNewsGroup
  382.  
  383.     | msgFile subjectString parenIdx fromString orgString |
  384.     number := aNumber.
  385.  
  386.     "DJM: should include error trap on open here - return nil if error"
  387.     msgFile := (aNewsGroup messageDirectory construct: aNumber printString) readStream.
  388.  
  389.     [
  390.     msgFile skipToAll: 'From: '; skipSeparators.
  391.     fromString := msgFile upTo: Character cr.
  392.     msgFile reset.
  393.     msgFile skipToAll: 'Subject: '; skipSeparators.
  394.     subjectString := msgFile upTo: Character cr.
  395.     msgFile reset.
  396.     msgFile skipToAll: 'Organization: '; skipSeparators.
  397.     orgString := msgFile upTo: Character cr.
  398.     ] valueNowOrOnUnwindDo: [msgFile close].
  399.  
  400.     subjectString isEmpty
  401.         ifTrue: [ subjectString := 'no subject' ]
  402.         ifFalse: [subjectString := subjectString copyFrom: 10 to: subjectString size ].
  403.     fromString isEmpty
  404.         ifTrue: [ fromString := 'no from line' ]
  405.         ifFalse: [  "If there is a left-paren, copy from that point on (hope for real name)"
  406.             ((parenIdx := fromString indexOf: $( ) == 0)
  407.                 ifTrue: [ fromString := fromString copyFrom: 7 to: fromString size ]
  408.                 ifFalse: [ fromString := fromString copyFrom: parenIdx to: fromString size ]].
  409.  
  410.     orgString isNil ifTrue: [ orgString := '' ].
  411.     orgString isEmpty
  412.         ifFalse: [orgString := orgString copyFrom: 14 to: orgString size ].
  413.  
  414.     printString := fromString, orgString.
  415.  
  416.     ^subjectString! !
  417.  
  418. !NewsMessage methodsFor: 'comparing'!
  419.  
  420. = aNewsMessage
  421.  
  422.     ^number = aNewsMessage asNumber! !
  423.  
  424. !NewsMessage methodsFor: 'converting'!
  425.  
  426. asInteger
  427.  
  428.     "Return my message number"
  429.  
  430.     ^number!
  431.  
  432. asNumber
  433.  
  434.     "Return my message number"
  435.  
  436.     ^number!
  437.  
  438. nameOfFile
  439.  
  440.     ^number printString! !
  441.  
  442. !NewsMessage methodsFor: 'printing'!
  443.  
  444. printString
  445.  
  446.     "String to appear in message pane of NewsReader.  This is set from the message file during initialization."
  447.  
  448.     ^printString! !
  449. "-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!
  450.  
  451. NewsMessage class
  452.     instanceVariableNames: ''!
  453.  
  454.  
  455. !NewsMessage class methodsFor: 'instance creation'!
  456.  
  457. named: aString cacheInGroup: aNewsGroup
  458.  
  459.     "Return an instance of this class representing the given message number in aNewsGroup.  Returns nil if an instance couldn't be created."
  460.  
  461.     | newMessage subjectString |
  462.     (self isMessageName: aString) ifFalse: [ ^nil ].
  463.     newMessage := self new.
  464.     subjectString := newMessage
  465.         initializeNumber: aString asNumber
  466.         group: aNewsGroup.
  467.  
  468.     "On second thought, a NewsMessage won't cache its subject
  469.     subject := aNewsGroup newOrExistingSubjectNamed: subjectString.
  470.     subject addMessage: self"
  471.  
  472.     (aNewsGroup newOrExistingSubjectNamed: subjectString)
  473.         addMessage: newMessage.
  474.     ^newMessage!
  475.  
  476. named: aString ifInSubject: aNewsSubject cacheInGroup: aNewsGroup
  477.  
  478.     "Return an instance of this class representing the given message number in aNewsGroup.  Returns nil if an instance couldn't be created."
  479.  
  480.     | newMessage subjectString |
  481.     (self isMessageName: aString) ifFalse: [ ^nil ].
  482.     newMessage := self new.
  483.     subjectString := newMessage
  484.         initializeNumber: aString asNumber
  485.         group: aNewsGroup.
  486.  
  487.     (aNewsSubject sameThreadAs: subjectString) ifTrue: [
  488.         (aNewsGroup newOrExistingSubjectNamed: subjectString)
  489.             addMessage: newMessage ].
  490.     ^newMessage! !
  491.  
  492. !NewsMessage class methodsFor: 'name verification'!
  493.  
  494. isMessageName: aString
  495.  
  496.     "Answer if aString is the name of a message file."
  497.  
  498.     ^aString first isDigit & aString last isDigit! !
  499.  
  500. Model subclass: #NewsReader
  501.     instanceVariableNames: 'subscribedGroup subject message chapter section subsection area '
  502.     classVariableNames: 'MessageTemplate NewsDirectory Subscribed '
  503.     poolDictionaries: ''
  504.     category: 'Tools-NewsReader'!
  505. NewsReader comment:
  506. 'Instances of this class allow reading and browsing and posting messages on UseNet in Smalltalk.  By Carl Watts (Carl@ParcPlace.com)
  507.  
  508. Modified by David Monnie (davidm@slc.com) July 7, 1991.
  509.     Added classes:
  510.         NewsGroup
  511.         NewsMessage
  512.         NewsSubject
  513.         SparseIntegerCollection
  514.     Added features:
  515.         Subject pane
  516.         Individual messages marked read (rather than just a high-water mark)
  517.         Ability to save setup to file in Smalltalk format or .newsrc format
  518.  
  519. The user interface is very Smalltalk browser like.  It allows you to read messages in any order you like just by clicking on the message subject.  You can name newsgroups that you wish to subscribe to.  And you can easily post new messages to these newsgroups.
  520.  
  521. Unlike other newsreaders, this one needs to have the message directories of the newsserver mounted on this machines filesystem in order to read/view the messages.  This newsreader will look for the message directories under ''/mnt/spool/news'' by default but you can set it to be whereever you like by sending a message to the class.  If I ever find out what the protocol is to communicate through a Unix Socket to a newsserver, I can easily modify this newsreader to user that medium.  But for now, it needs to access the files directly.
  522.  
  523. For posting messages, this newsreader uses the standard "inews" command at the OS level (if one can use the term "OS" so broadly as to include Unix).  "inews" must be available in the current path.
  524.  
  525. This newsreader also keeps a standard inews message header that you can customize with your personal information.
  526.  
  527. When you open a new instance of the receiver, there are three views, two ListViews at the top and a TextView of the bottom.  The first ListView lists the newsgroups you currently subscribe to.  You can subscribe to new ones by selecting ''subscribe...'' from the menu of this view.  The second ListView shows the messages (by Subject) in the currently selected newsgroup (selected in the first ListView).  The TextView shows you the text of the message if a message is selected.  Or it shows a preformated header to post a message to the currently selected newsgroup, if one is selected.  Or if no newsgroup is selected then it shows you the default header for messages.  You can change this default header however you like and ''accept'' it.
  528.  
  529. To post a new message to a newsgroup, select the newsgroup (and don''t select any messages in the newsgroup).  The TextView will show a preformatted message header for a new message.  Add the text that you wish after the header (leaving at least one blank line between the header and the start of your message).  When you are satsified with your message, select ''post'' from the menu and your new message will be posted to that newsgroup.
  530.  
  531. This class was created by Carl Watts (Carl@ParcPlace.com).  If you like this class and use it, send me an email message about it. '!
  532.  
  533.  
  534. !NewsReader methodsFor: 'subscribed group list'!
  535.  
  536. newSubscribedGroupList
  537.  
  538.     "Update the subscribedGroupList."
  539.  
  540.     self changed: #subscribedGroup!
  541.  
  542. newSubscribedGroupList: initialSelection
  543.  
  544. "Update the subscribedGroupList and make initialSelection the initial selection."
  545.  
  546.     subscribedGroup := initialSelection.
  547.     self newSubscribedGroupList!
  548.  
  549. subscribedGroup
  550.  
  551. "Answer the currently selected subscribed group."
  552.  
  553.     ^subscribedGroup!
  554.  
  555. subscribedGroup: aGroup
  556.  
  557. "Set the currently selected subscribed group."
  558.  
  559.     subscribedGroup := aGroup.
  560.     self newSubjectList!
  561.  
  562. subscribedGroupList
  563.  
  564.     "Answer the list of subscribed groups."
  565.  
  566.     ^Array withAll: Subscribed!
  567.  
  568. subscribedGroupMenu
  569.  
  570. "Answer the menu for the subscribed group list."
  571.  
  572.     self subscribedGroup isNil ifTrue: [^PopUpMenu
  573.         labelList: #(('update') ('subscriptions...') ('save setup to file' 'retrieve setup from file'))
  574.         values: #(updateSubscribedList openGroupBrowser saveSetup retrieveSetup)].
  575.     ^PopUpMenu
  576.         labelList: #(('find message') ('revive group' 'catch up' update) ('unsubscribe'))
  577.         values: #(findMessage unreadAll catchUp updateGroup unsubscribeGroup)! !
  578.  
  579. !NewsReader methodsFor: 'subscribed group functions'!
  580.  
  581. catchUp
  582.  
  583.     "Mark all messages in this newsgroup as read, and refresh the subject view"
  584.  
  585.     subscribedGroup catchUp.
  586.     self newSubjectList!
  587.  
  588. findMessage
  589.  
  590.     "Allow user to select from list of messages.  Update view to the selection."
  591.  
  592.     | subjectsAndMessages menu selectedIndex labels cr |
  593.     subjectsAndMessages := OrderedCollection new.
  594.     subscribedGroup subjects do: [ :s |
  595.         s messages do: [ :m |
  596.             subjectsAndMessages add: (Array with: s with: m) ]].
  597.     subjectsAndMessages := Array withAll: subjectsAndMessages.
  598.     labels := '' asText.
  599.     cr := String with: Character cr.
  600.     subjectsAndMessages do: [ :sm |
  601.         labels := labels, cr, (sm at: 2) printString asText, '    ', (sm at: 1) printString asText allBold ].
  602.     labels isEmpty ifTrue: [ ^nil ].
  603.     menu := PopUpMenu labels: (labels copyFrom: 2 to: labels size).
  604.     selectedIndex := menu startUpWithHeading: 'Pick a message'.
  605.     "Did the user cancel?"
  606.     selectedIndex isNil ifTrue: [ ^nil ].
  607.  
  608.     subject := (subjectsAndMessages at: selectedIndex) at: 1.
  609.     self changed: #subject.
  610.  
  611.     message := (subjectsAndMessages at: selectedIndex) at: 2.
  612.     self changed: #message!
  613.  
  614. openGroupBrowser
  615.  
  616. "Open a browser on all the groups accessable by the receiver."
  617.  
  618.     self class openAllGroupViewOn: self!
  619.  
  620. retrieveSetup
  621.  
  622. "Retrieve the current settings for NewsReader from a file."
  623.  
  624.     self class retrieveSetup.
  625.     self newSubscribedGroupList!
  626.  
  627. saveSetup
  628.  
  629. "Save the current settings of the receiver to a file so they can be later retrieved."
  630.  
  631.     self updateSubscribedList. "Empties caches, so we don't try to write them out to file"
  632.     self class saveSetup!
  633.  
  634. unreadAll
  635.  
  636.     "Unread all the messages that have been read in this group.  This will cause all available subjects in this group to appear in the subject list."
  637.  
  638.     subscribedGroup unreadAll.
  639.     self newSubjectList!
  640.  
  641. updateGroup
  642.  
  643.     "Empty all subscribed NewsGroup caches, and refresh display."
  644.  
  645.     subscribedGroup emptyCaches.
  646.     self newSubjectList!
  647.  
  648. updateSubscribedList
  649.  
  650.     "Empty all subscribed NewsGroup caches, and refresh display."
  651.  
  652.     Subscribed do: [ :aNewsGroup | aNewsGroup emptyCaches ].
  653.     self newSubscribedGroupList! !
  654.  
  655. !NewsReader methodsFor: 'subject list'!
  656.  
  657. newSubjectList
  658.  
  659.     "Refresh the subject list from currently selected newsgroup"
  660.  
  661.     self changed: #subject!
  662.  
  663. subject
  664.  
  665.     ^subject!
  666.  
  667. subject: aNewsSubject
  668.  
  669.     subject := aNewsSubject.
  670.     self newMessageList!
  671.  
  672. subjectList
  673.  
  674.     "Answer the SequencableCollection of subjects."
  675.  
  676.     ^subscribedGroup isNil
  677.         ifTrue: [ #() ]
  678.         ifFalse: [ subscribedGroup refreshedSubjects ]!
  679.  
  680. subjectMenu
  681.  
  682.     "Answer the menu for the message view."
  683.  
  684.     subject isNil
  685.         ifTrue: [^PopUpMenu
  686.             labelList: #(('revive a subject'))
  687.             values: #( #reviveASubject ) ]
  688.         ifFalse: [^PopUpMenu
  689.             labelList: #((kill 'unread' 'revive subject'))
  690.             values: #( killCurrentSubject unreadCurrentSubject reviveCurrentSubject)]! !
  691.  
  692. !NewsReader methodsFor: 'subject list functions'!
  693.  
  694. killCurrentSubject
  695.  
  696.     "Mark all messages under current subject as read, and remove from view."
  697.  
  698.     subscribedGroup markSubjectAsRead: subject.
  699.     self newSubjectList!
  700.  
  701. reviveASubject
  702.  
  703.     "Present a list of all available subjects in the current group, and 'unread' all messages in the subject selected from that list."
  704.  
  705.     | allSubjects menu selectedIndex |
  706.     allSubjects := subscribedGroup allSubjectsReadOrUnread.
  707.     menu := PopUpMenu labelList: (Array with: (allSubjects collect: [ :s | s printString ])).
  708.     selectedIndex :=menu startUpWithHeading: 'Pick a subject'.
  709.     "Did the user cancel?"
  710.     selectedIndex isNil ifTrue: [ ^nil ].
  711.  
  712.     subject := allSubjects at: selectedIndex.
  713.     subscribedGroup addSubject: subject.
  714.     "Make sure 'subject' is in subject list, and is highlighted"
  715.     self changed: #subject!
  716.  
  717. reviveCurrentSubject
  718.  
  719.     "Read all available messages, and 'unread' all messages in current subject."
  720.  
  721.     subscribedGroup reviveSubject: subject.
  722.     "Make sure 'subject' is in subject list, and is highlighted"
  723.     self changed: #subject!
  724.  
  725. unreadCurrentSubject
  726.  
  727.     "Remove all messages in current subject from the 'marked read' list"
  728.  
  729.     subscribedGroup markSubjectAsUnread: subject! !
  730.  
  731. !NewsReader methodsFor: 'message list'!
  732.  
  733. message
  734.  
  735. "Answer the current message."
  736.  
  737.     ^message!
  738.  
  739. message: aMessage
  740.  
  741. "Set the current message."
  742.  
  743.     message := aMessage.
  744.     message notNil ifTrue: [self readMessage].
  745.     self newText!
  746.  
  747. messageList
  748.  
  749.     "Answer the SequencableCollection of messages."
  750.  
  751.     ^subject isNil
  752.         ifTrue: [ #() ]
  753.         ifFalse: [ subject messages copy ]!
  754.  
  755. messageMenu
  756.  
  757. "Answer the menu for the message view."
  758.  
  759.     message isNil
  760.         ifTrue: [^nil]
  761.         ifFalse: [^PopUpMenu
  762.             labelList: #(('save as...' unread 'hardcopy') ('reply' 'followup'))
  763.             values: #( saveMessage unreadMessage hardcopyMessage replyMessage followupMessage )]!
  764.  
  765. newMessageList
  766.  
  767.     "Show a new message list."
  768.  
  769.     self changed: #message!
  770.  
  771. newMessageList: initialSelection
  772.  
  773. "Show a new message list with initialSelection as the first selection."
  774.  
  775.     message := initialSelection.
  776.     self newMessageList! !
  777.  
  778. !NewsReader methodsFor: 'message list functions'!
  779.  
  780. followupMessage
  781.  
  782.     "Post a followup to the current message"
  783.  
  784.     ^self error: 'Not yet implemented'!
  785.  
  786. hardcopyMessage
  787.  
  788. "Print a copy of the message to the printer."
  789.  
  790.     | document |
  791.     document := Document new.
  792.     document startParagraph.
  793.     document addText: self text.
  794.     document close.
  795.     document toPrinter!
  796.  
  797. readMessage
  798.  
  799.     "Mark the currently selected message as being read."
  800.  
  801.     self subscribedGroup markAsRead: message.
  802.     "subject removeMessage: message"!
  803.  
  804. replyMessage
  805.  
  806.     "Send a mail reply to the author of the current message"
  807.  
  808.     | mr lw activeMr |
  809.     mr := Smalltalk at: #MailReader ifAbsent: [ nil ].
  810.     lw := Smalltalk at: #LetterWriter ifAbsent: [ nil ].
  811.     (mr isNil | lw isNil) ifTrue: [ ^self error: 'Mail utility not installed' ].
  812.  
  813.     "If there is an active mail reader, get it.  Letter writer works better with one."
  814.     activeMr := mr activeInstance.
  815.     activeMr isNil ifTrue: [[ mr open ] fork ].
  816.     [ activeMr isNil ] whileTrue: [
  817.         (Delay forSeconds: 1) wait.
  818.         activeMr := mr activeInstance ].
  819.  
  820.     lw openOn: (lw new
  821.         header: self textForMessageReplyHeader
  822.         body: self textForMessage
  823.         mailReader: activeMr )!
  824.  
  825. saveMessage
  826.  
  827.     "Save the currently selected message to a file."
  828.  
  829.     subscribedGroup saveMessage: message!
  830.  
  831. unreadMessage
  832.  
  833.     "Mark the currently selected message as NOT being read."
  834.  
  835.     self subscribedGroup markAsUnread: message! !
  836.  
  837. !NewsReader methodsFor: 'message template'!
  838.  
  839. acceptMessageTemplateText: aText from: aController
  840.  
  841. "Accept changed text for the message template from aController."
  842.  
  843.     aText isEmpty ifTrue: [^true].
  844.     MessageTemplate := aText asString.
  845.     ^true!
  846.  
  847. messageTemplate
  848.  
  849. "Answer the string that is the message template."
  850.  
  851.     ^MessageTemplate!
  852.  
  853. textForMessageTemplate
  854.  
  855. "Answer the text for the message template."
  856.  
  857.     ^self messageTemplate asText allBold! !
  858.  
  859. !NewsReader methodsFor: 'text'!
  860.  
  861. acceptText: aText from: aController
  862.  
  863. "Accept changed text from aController."
  864.  
  865.     self amViewingBlankMessage ifTrue: [^self postMessageText: aText from: aController].
  866.     ^self acceptMessageTemplateText: aText from: aController!
  867.  
  868. doItContext
  869.  
  870.     ^nil!
  871.  
  872. doItReceiver
  873.  
  874.     ^nil!
  875.  
  876. newText
  877.  
  878. "Show a new text."
  879.  
  880.     self changed: #text!
  881.  
  882. text
  883.  
  884. "Answer the text to be displayed in the text view."
  885.  
  886.     self amViewingMessage ifTrue: [^self textForMessage].
  887.     self amViewingBlankMessage ifTrue: [^self textForBlankMessage].
  888.     ^self textForMessageTemplate!
  889.  
  890. textForMessage
  891.  
  892.     "Answer the text for the message currently being viewed."
  893.  
  894.     ^subscribedGroup textForMessage: message!
  895.  
  896. textForMessageReplyHeader
  897.  
  898.     | cr msgTextStream from |
  899.     cr := String with: Character cr.
  900.     msgTextStream := self textForMessage asString readStream.
  901.     msgTextStream skipToAll: 'From: '; skipSeparators.
  902.     from := msgTextStream upTo: Character cr.
  903.     ^    'Subject: ', subject subjectStringWithOneRe, cr,
  904.         'To: ', (from copyFrom: 7 to: from size), cr,
  905.         'cc: ' asText!
  906.  
  907. textMenu
  908.  
  909. "Answer the menu for the text view."
  910.  
  911.     self amViewingMessage ifTrue: [^self textMenuForMessage].
  912.     self amViewingBlankMessage ifTrue: [^self textMenuForBlankMessage].
  913.     ^self textMenuForMessageTemplate!
  914.  
  915. textMenuForBlankMessage
  916.  
  917. "Answer the menu for the text view if a blank message is currently being viewed/edited."
  918.  
  919.     ^PopUpMenu
  920.         labelList: #(('again' 'undo') ('copy' 'cut' 'paste') ('post' 'cancel'))
  921.         values: #(again undo copySelection cut paste accept cancel)!
  922.  
  923. textMenuForMessage
  924.  
  925. "Answer the menu for the text view if a message is currently being viewed."
  926.  
  927.     ^PopUpMenu
  928.         labelList: #(('again') (copy cut paste) ('do it' 'print it' inspect) ('cancel') ('hardcopy'))
  929.         values: #(again copySelection cut paste doIt printIt inspectIt cancel hardcopy)!
  930.  
  931. textMenuForMessageTemplate
  932.  
  933. "Answer the menu for the text view if the message is currently being viewed/edited."
  934.  
  935.     ^PopUpMenu
  936.         labelList: #(('again' 'undo') ('copy' 'cut' 'paste') ('accept' 'cancel'))
  937.         values: #(again undo copySelection cut paste accept cancel)! !
  938.  
  939. !NewsReader methodsFor: 'area list'!
  940.  
  941. area
  942.  
  943. "Answer the current area."
  944.  
  945.     ^area!
  946.  
  947. area: anArea
  948.  
  949. "Set the current area."
  950.  
  951.     area := anArea.
  952.     self newChapterList: chapter!
  953.  
  954. areaList
  955.  
  956. "Answer the SequencableCollection of areas."
  957.  
  958.     | dir contents |
  959.     dir := NewsDirectory.
  960.     contents := dir directoryContents
  961.         select: [:each | self isGroupName: each].
  962.     ^contents asSortedCollection!
  963.  
  964. areaMenu
  965.  
  966. "Answer the menu for the area view."
  967.  
  968.     area isNil ifTrue: [^nil].
  969.     chapter isNil ifFalse: [^nil].
  970.     ^PopUpMenu
  971.         labelList: #(('subscribe'))
  972.         values: #(subscribeGroup)!
  973.  
  974. newAreaList: initialSelection
  975.  
  976. "Show a new area list with initialSelection as the first selection."
  977.  
  978.     area := initialSelection.
  979.     self changed: #area! !
  980.  
  981. !NewsReader methodsFor: 'testing'!
  982.  
  983. amViewingBlankMessage
  984.  
  985. "Answer if the user is currently viewing a blank message."
  986.  
  987.     ^self message isNil & subject notNil!
  988.  
  989. amViewingMessage
  990.  
  991. "Answer if the user is currently view a message."
  992.  
  993.     ^self message notNil!
  994.  
  995. amViewingMessageTemplate
  996.  
  997. "Answer if the user is currently viewing the blank message template."
  998.  
  999.     ^self message isNil & self subscribedGroup isNil!
  1000.  
  1001. isGroupName: aString
  1002.  
  1003. "Answer if this is part of the name of a group (not a message name)."
  1004.  
  1005.     ^(self isMessageName: aString) not and: [aString first ~= $.]!
  1006.  
  1007. isMessageName: aString
  1008.  
  1009. "Answer if aString is the name of a message file."
  1010.  
  1011.     ^aString first isDigit & aString last isDigit! !
  1012.  
  1013. !NewsReader methodsFor: 'subscription'!
  1014.  
  1015. amSubscribedGroup: aGroup
  1016.  
  1017. "Answer if the group aGroup is subscribed to."
  1018.  
  1019.     ^Subscribed includes: aGroup!
  1020.  
  1021. currentGroup
  1022.  
  1023. "Answer the current group (newsgroup)."
  1024.  
  1025.     | groupName |
  1026.  
  1027.     groupName := ''.
  1028.     area notNil ifTrue: [groupName := area].
  1029.     chapter notNil ifTrue: [groupName := groupName, '.', chapter].
  1030.     section notNil ifTrue: [groupName := groupName, '.', section].
  1031.     subsection notNil ifTrue: [groupName := groupName, '.', subsection].
  1032.     ^NewsGroup named: groupName!
  1033.  
  1034. subscribeGroup
  1035.  
  1036. "Begin subscribing to the current group."
  1037.  
  1038.     self subscribeGroup: self currentGroup!
  1039.  
  1040. subscribeGroup: aGroup
  1041.  
  1042. "Begin subscribing to aGroup."
  1043.  
  1044.     (self amSubscribedGroup: aGroup) ifFalse: [
  1045.         Subscribed add: aGroup.
  1046.         self subscribedGroup isNil ifTrue: [self newSubscribedGroupList: nil]]!
  1047.  
  1048. unsubscribeGroup
  1049.  
  1050. "Unsubscribe to the current group."
  1051.  
  1052.     self unsubscribeGroup: self subscribedGroup!
  1053.  
  1054. unsubscribeGroup: aGroup
  1055.  
  1056. "Unsubscribe to the group aGroup."
  1057.  
  1058.     (self amSubscribedGroup: aGroup) ifTrue: [
  1059.         Subscribed remove: aGroup.
  1060.         self newSubscribedGroupList: nil]! !
  1061.  
  1062. !NewsReader methodsFor: 'new messages'!
  1063.  
  1064. readMessage: aMessageNumber group: aGroup
  1065.  
  1066. "I have read the specified message in aGroup."
  1067. "Update the read entry for that group."
  1068.  
  1069.     aGroup markAsRead: self message!
  1070.  
  1071. updateNewMessageGroupsIn: aDirectory group: aGroup
  1072.  
  1073. "Update the NewMessageGroups in aDirectory."
  1074.  
  1075.     | oldHigh contents thisGroup |
  1076.  
  1077.     contents := aDirectory directoryContents.
  1078.     oldHigh := self highestReadIn: aGroup.
  1079.     (contents detect: [:each | (self isMessageName: each) and: [each asNumber > oldHigh]] ifNone: [nil]) notNil
  1080.         ifTrue: [self newMessagesIn: aGroup].
  1081.     contents do: [:each | 
  1082.         (self isGroupName: each) ifTrue: [
  1083.             thisGroup := (aGroup isEmpty) ifTrue: [each] ifFalse: [aGroup, '.', each].
  1084.             (self amSubscribedGroup: thisGroup) ifTrue: [
  1085.                 self updateNewMessageGroupsIn: (aDirectory construct: each) group: thisGroup]]]! !
  1086.  
  1087. !NewsReader methodsFor: 'blank message'!
  1088.  
  1089. postMessageText: aText from: aController
  1090.  
  1091. "Post the message in aText.  aText came from aController."
  1092.  
  1093.     | fileName fileStream resultString |
  1094.     aText isEmpty ifTrue: [^true].
  1095.     fileName := 'temp news' asFilename.
  1096.     fileStream := fileName writeStream.
  1097.     [fileStream nextPutAll: aText asString] valueNowOrOnUnwindDo: [fileStream close].
  1098.  
  1099.     "Last chance to change your mind"
  1100.     (DialogView
  1101.         confirm: 'Do you really want to post this?'
  1102.         initialAnswer: false)
  1103.     ifFalse: [ ^true ].
  1104.  
  1105.     resultString := UnixProcess cshOne: 'inews "', fileName asString, '"'.
  1106.     resultString isEmpty
  1107.         ifTrue: [
  1108.             fileName delete.
  1109.             self newMessageList: message.
  1110.             ^true]
  1111.         ifFalse: [
  1112.             [self error: resultString] fork.
  1113.             ^false]!
  1114.  
  1115. textForBlankMessage
  1116.  
  1117. "Answer the text for a blank message for the current group."
  1118.  
  1119.     | cr |
  1120.  
  1121.     cr := String with: Character cr.
  1122.     ^('Newsgroups: ', self subscribedGroup printString, cr,
  1123.         'Subject: ', cr,
  1124.         self messageTemplate, cr, cr) asText! !
  1125.  
  1126. !NewsReader methodsFor: 'chapter list'!
  1127.  
  1128. chapter
  1129.  
  1130. "Answer the current chapter."
  1131.  
  1132.     ^chapter!
  1133.  
  1134. chapter: aChapter
  1135.  
  1136. "Set the current chapter."
  1137.  
  1138.     chapter := aChapter.
  1139.     self newSectionList: section!
  1140.  
  1141. chapterDirectory
  1142.  
  1143. "Answer the directory where the chapters appears."
  1144.  
  1145.     | dir |
  1146.     area isNil ifTrue: [^nil].
  1147.     dir := NewsDirectory construct: area.
  1148.     dir isReadable
  1149.         ifTrue: [^dir]
  1150.         ifFalse: [^nil]!
  1151.  
  1152. chapterList
  1153.  
  1154. "Answer the SequencableCollection of chapters."
  1155.  
  1156.     | dir contents |
  1157.  
  1158.     dir := self chapterDirectory.
  1159.     dir isNil ifTrue: [^nil].
  1160.     contents := dir directoryContents
  1161.         select: [:each | self isGroupName: each].
  1162.     ^contents asSortedCollection!
  1163.  
  1164. chapterMenu
  1165.  
  1166. "Answer the menu for the chapter view."
  1167.  
  1168.     chapter isNil ifTrue: [^nil].
  1169.     section isNil ifFalse: [^nil].
  1170.     ^PopUpMenu
  1171.         labelList: #(('subscribe' 'unsubscribe'))
  1172.         values: #(subscribeGroup unsubscribeGroup)!
  1173.  
  1174. newChapterList: initialSelection
  1175.  
  1176. "Show a new chapter list with initialSelection as the first selection."
  1177.  
  1178.     chapter := initialSelection.
  1179.     self changed: #chapter! !
  1180.  
  1181. !NewsReader methodsFor: 'section list'!
  1182.  
  1183. newSectionList: initialSelection
  1184.  
  1185. "Show a new section list with initialSelection as the first selection."
  1186.  
  1187.     section := initialSelection.
  1188.     self changed: #section!
  1189.  
  1190. section
  1191.  
  1192. "Answer the current section."
  1193.  
  1194.     ^section!
  1195.  
  1196. section: aSection
  1197.  
  1198. "Set the current section."
  1199.  
  1200.     section := aSection.
  1201.     self newSubsectionList: subsection!
  1202.  
  1203. sectionDirectory
  1204.  
  1205. "Answer the directory where the sections appear."
  1206.  
  1207.     chapter isNil ifTrue: [^nil].
  1208.     ^(self chapterDirectory) construct: chapter!
  1209.  
  1210. sectionList
  1211.  
  1212. "Answer the SequencableCollection of sections."
  1213.  
  1214.     | dir contents |
  1215.  
  1216.     dir := self sectionDirectory.
  1217.     dir isNil ifTrue: [^nil].
  1218.     contents := dir directoryContents
  1219.         select: [:each | self isGroupName: each].
  1220.     ^contents asSortedCollection!
  1221.  
  1222. sectionMenu
  1223.  
  1224. "Answer the menu for the section view."
  1225.  
  1226.     section isNil ifTrue: [^nil].
  1227.     subsection isNil ifFalse: [^nil].
  1228.     ^PopUpMenu
  1229.         labelList: #(('subscribe' 'unsubscribe'))
  1230.         values: #(subscribeGroup unsubscribeGroup)! !
  1231.  
  1232. !NewsReader methodsFor: 'subsection list'!
  1233.  
  1234. newSubsectionList: initialSelection
  1235.  
  1236. "Show a new subsection list with initialSelection as the first selection."
  1237.  
  1238.     subsection := initialSelection.
  1239.     self changed: #subsection!
  1240.  
  1241. subsection
  1242.  
  1243. "Answer the current subsection."
  1244.  
  1245.     ^subsection!
  1246.  
  1247. subsection: aSubsection
  1248.  
  1249. "Set the current subsection."
  1250.  
  1251.     subsection := aSubsection!
  1252.  
  1253. subsectionDirectory
  1254.  
  1255. "Answer the directory where the subsections appear."
  1256.  
  1257.     section isNil ifTrue: [^nil].
  1258.     ^(self sectionDirectory) construct: section!
  1259.  
  1260. subsectionList
  1261.  
  1262. "Answer the SequencableCollection of subsections."
  1263.  
  1264.     | dir contents |
  1265.  
  1266.     dir := self subsectionDirectory.
  1267.     dir isNil ifTrue: [^nil].
  1268.     contents := dir directoryContents
  1269.         select: [:each | self isGroupName: each].
  1270.     ^contents asSortedCollection!
  1271.  
  1272. subsectionMenu
  1273.  
  1274. "Answer the menu for the subsection view."
  1275.  
  1276.     subsection isNil ifTrue: [^nil].
  1277.     ^PopUpMenu
  1278.         labelList: #(('subscribe' 'unsubscribe'))
  1279.         values: #(subscribeGroup unsubscribeGroup)! !
  1280. "-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!
  1281.  
  1282. NewsReader class
  1283.     instanceVariableNames: ''!
  1284.  
  1285.  
  1286. !NewsReader class methodsFor: 'setup'!
  1287.  
  1288. defaultSetupFile
  1289.  
  1290. "Answer the filename of the default setup file."
  1291.  
  1292.     ^Filename named: 'NewsReaderSetup'!
  1293.  
  1294. newsDirectory
  1295.  
  1296. "Answer the filename of the directory where the news files are maintained."
  1297.  
  1298.     ^NewsDirectory!
  1299.  
  1300. newsDirectory: aFilename
  1301.  
  1302. "Set the filename of the directory where the news files are maintained."
  1303. "NewsReader newsDirectory: '/usr/spool/news'"
  1304.  
  1305.     NewsDirectory := aFilename asFilename!
  1306.  
  1307. retrieveSetup
  1308.  
  1309. "Retrieve the current settings (subscriptions, etc.) from a file called 'NewsReaderSetup'."
  1310.  
  1311.     | retrieveFilename |
  1312.     self defaultSetupFile isReadable
  1313.         ifTrue: [^self retrieveSetupFrom: self defaultSetupFile].
  1314.  
  1315.     retrieveFilename := DialogView request: 'Name of NewsReader Setup file?'.
  1316.     retrieveFilename isEmpty ifFalse: [self retrieveSetupFrom: retrieveFilename asFilename].!
  1317.  
  1318. retrieveSetupFrom: aFilename
  1319.  
  1320.     "Retrieve the current settings (subscriptions, etc.) from a file."
  1321.  
  1322.     | stream |
  1323.     stream := aFilename asFilename readStream.
  1324.     [(Number readFrom: stream) = 1
  1325.         ifTrue: [
  1326.              self newsDirectory: (String readFrom: stream).
  1327.              MessageTemplate := String readFrom: stream.
  1328.              Subscribed := Set readFrom: stream ]
  1329.         ifFalse: [ "Try rn format"
  1330.             self retrieveSetupRnStyleFrom: stream ]] valueNowOrOnUnwindDo: [stream close]!
  1331.  
  1332. retrieveSetupRnStyleFrom: stream
  1333.  
  1334.     "Retrieve the current subscriptions and read messages list in .newsrc format from stream"
  1335.  
  1336.     | line newSubscribed |
  1337.     newSubscribed := Set new.
  1338.     [ stream atEnd ] whileFalse: [
  1339.         line := stream upTo: Character cr.
  1340.         (line includes: $:) ifTrue: [ "subscribed"
  1341.             newSubscribed add: (NewsGroup fromRnString: line) ]].
  1342.     Subscribed := newSubscribed!
  1343.  
  1344. saveSetup
  1345.  
  1346. "Save the current settings (subscriptions, etc.) to a file which can be retrieved (via retrieveSetup) later."
  1347.  
  1348.     format := (PopUpMenu labelList: #((smalltalk readnews))) startUpWithHeading: 'Select format'.
  1349.     format = 1
  1350.         ifTrue: [ "Smalltalk"
  1351.             self saveSetupAs: self defaultSetupFile ]
  1352.         ifFalse: [
  1353.             self saveSetupRnStyleAs: self defaultSetupFile ]!
  1354.  
  1355. saveSetupAs: aFilename
  1356.  
  1357. "Save the current settings (subscriptions, etc.) to a file which can be retrieved (via retrieveSetup) later."
  1358.  
  1359.     | stream |
  1360.     stream := aFilename asFilename writeStream.
  1361.     [    1 storeOn: stream. stream space.
  1362.         NewsDirectory asString storeOn: stream. stream space.
  1363.         MessageTemplate storeOn: stream. stream space.
  1364.         Subscribed storeOn: stream.
  1365.     ] valueNowOrOnUnwindDo: [stream close]!
  1366.  
  1367. saveSetupRnStyleAs: aFilename
  1368.  
  1369. "Save the current settings (subscriptions, etc.) to a file which can be retrieved (via retrieveSetup) later."
  1370.  
  1371.     | stream |
  1372.     stream := aFilename asFilename writeStream.
  1373.     [    Subscribed do: [ :aNewsGroup |
  1374.             aNewsGroup storeRnStyleOn: stream.
  1375.             stream cr ].
  1376.     ] valueNowOrOnUnwindDo: [stream close]! !
  1377.  
  1378. !NewsReader class methodsFor: 'scheduling'!
  1379.  
  1380. open
  1381.  
  1382.     "Open a new instance of the receiver."
  1383.     "NewsReader open"
  1384.  
  1385.     self openSubscribedGroupViewOn: self new updateSubscribedList!
  1386.  
  1387. openAllGroupViewOn: aNewsReader
  1388.  
  1389. "Open a view on aNewsReader showing all accessable newsgroups."
  1390.  
  1391.     | topView chapterView sectionView subsectionView areaView topWindow |
  1392.  
  1393.     topWindow := ScheduledWindow new
  1394.                             model: aNewsReader;
  1395.                             label: self name, ' ', NewsDirectory asString;
  1396.                             minimumSize: 400@100.
  1397.     topView := DependentComposite new.
  1398.     topWindow component: topView.
  1399.  
  1400.     areaView := SelectionInListView on: aNewsReader
  1401.         aspect: #area change: #area:
  1402.         list: #areaList menu: #areaMenu initialSelection: #area.
  1403.     topView
  1404.         add: (LookPreferences edgeDecorator on: areaView)
  1405.         in: (0@0 corner: 0.25@1).
  1406.  
  1407.     chapterView := SelectionInListView on: aNewsReader
  1408.         aspect: #chapter change: #chapter:
  1409.         list: #chapterList menu: #chapterMenu initialSelection: #chapter.
  1410.     topView
  1411.         add: (LookPreferences edgeDecorator on: chapterView)
  1412.         in: (0.25@0 corner: 0.5@1).
  1413.  
  1414.     sectionView := SelectionInListView on: aNewsReader
  1415.         aspect: #section change: #section:
  1416.         list: #sectionList menu: #sectionMenu initialSelection: #section.
  1417.     topView
  1418.         add: (LookPreferences edgeDecorator on: sectionView)
  1419.         in: (0.5@0 corner: 0.75@1).
  1420.  
  1421.     subsectionView := SelectionInListView on: aNewsReader
  1422.         aspect: #subsection change: #subsection:
  1423.         list: #subsectionList menu: #subsectionMenu initialSelection: #subsection.
  1424.     topView
  1425.         add: (LookPreferences edgeDecorator on: subsectionView)
  1426.         in: (0.75@0 corner: 1@1).
  1427.  
  1428.     topWindow open!
  1429.  
  1430. openSubscribedGroupViewOn: aNewsReader
  1431.  
  1432.     "Open a view on the subscribed groups accessable by aNewsReader."
  1433.     "NewsReader open"
  1434.  
  1435.     | topView messageView textView subscribedGroupView topWindow subjectView |
  1436.  
  1437.     topWindow := ScheduledWindow new
  1438.                             model: aNewsReader;
  1439.                             label: self name, ' ', NewsDirectory asString;
  1440.                             minimumSize: 600@400.
  1441.     topView := DependentComposite new.
  1442.     topWindow component: topView.
  1443.  
  1444.     subscribedGroupView := SelectionInListView on: aNewsReader
  1445.         printItems: true oneItem: false aspect: #subscribedGroup change: #subscribedGroup:
  1446.         list: #subscribedGroupList menu: #subscribedGroupMenu initialSelection: #subscribedGroup.
  1447.     topView
  1448.         add: (LookPreferences edgeDecorator on: subscribedGroupView)
  1449.         in: (0@0 corner: 0.25@0.25).
  1450.  
  1451.     subjectView := SelectionInListView on: aNewsReader
  1452.         printItems: true oneItem: false aspect: #subject change: #subject:
  1453.         list: #subjectList menu: #subjectMenu initialSelection: #subject.
  1454.     topView
  1455.         add: (LookPreferences edgeDecorator on: subjectView)
  1456.         in: (0.25@0 corner: 0.70@0.25).
  1457.  
  1458.     messageView := SelectionInListView on: aNewsReader
  1459.         printItems: true oneItem: false
  1460.         aspect: #message change: #message:
  1461.         list: #messageList menu: #messageMenu initialSelection: #message.
  1462.     topView
  1463.         add: (LookPreferences edgeDecorator on: messageView)
  1464.         in: (0.70@0 corner: 1@0.25).
  1465.  
  1466.     textView := TextView on: aNewsReader
  1467.         aspect: #text change: #acceptText:from:
  1468.         menu: #textMenu.
  1469.     topView add: (LookPreferences edgeDecorator on: textView) in: (0@0.25 corner: 1@1).
  1470.  
  1471.     topWindow open! !
  1472.  
  1473. !NewsReader class methodsFor: 'initialization'!
  1474.  
  1475. initialize
  1476.  
  1477. "NewsReader initialize"
  1478.  
  1479.     self newsDirectory isNil ifTrue: [self newsDirectory: '/usr/spool/news'].
  1480.     Subscribed isNil ifTrue: [Subscribed := Set new].
  1481.     MessageTemplate isNil ifTrue: [self initializeMessageTemplate]!
  1482.  
  1483. initializeMessageTemplate
  1484.  
  1485. "Intialize the default message template for new messages."
  1486.  
  1487.     MessageTemplate :=
  1488. 'From: myAddress (My Name)
  1489. Organization: My Organization'! !
  1490.  
  1491. Object subclass: #NewsSubject
  1492.     instanceVariableNames: 'newsgroup subjectString messages '
  1493.     classVariableNames: ''
  1494.     poolDictionaries: ''
  1495.     category: 'Tools-NewsReader'!
  1496.  
  1497.  
  1498. !NewsSubject methodsFor: 'initializing'!
  1499.  
  1500. initializeWithString: aString newsGroup: aNewsGroup
  1501.  
  1502.     subjectString := aString.
  1503.     newsgroup := aNewsGroup.
  1504.     messages := OrderedCollection new!
  1505.  
  1506. setGroup: aNewsGroup
  1507.  
  1508.     newsgroup := aNewsGroup! !
  1509.  
  1510. !NewsSubject methodsFor: 'comparing'!
  1511.  
  1512. = aNewsSubject
  1513.  
  1514.     "Two NewsSubjects are equal if they are from the same newsgroup with the same subject string (ignoring an preceding 'Re:'s"
  1515.  
  1516.     ^(aNewsSubject isMemberOf: self class)
  1517.         ifTrue: [
  1518.             (aNewsSubject isFromGroup: newsgroup) &
  1519.             (aNewsSubject sameThreadAs: subjectString) ]
  1520.         ifFalse: [ false ]!
  1521.  
  1522. sameThreadAs: aString
  1523.  
  1524.     "True if my subjectString looks like aString, ignoring preceding 'Re:'s"
  1525.  
  1526.     | mySubject otherSubject |
  1527.     mySubject := self stripReFrom: subjectString.
  1528.     otherSubject := self stripReFrom: aString.
  1529.     ^(mySubject spellAgainst: otherSubject) > 80!
  1530.  
  1531. stripReFrom: aString
  1532.  
  1533.     "Return aString, with any leading 'Re:'s stripped"
  1534.  
  1535.     " NewsSubject new stripReFrom: 'hello' "
  1536.     " NewsSubject new stripReFrom: '  hello' "
  1537.     " NewsSubject new stripReFrom: 'Re: hello' "
  1538.     " NewsSubject new stripReFrom: '   Re:hello' "
  1539.     " NewsSubject new stripReFrom: '   Re: Re:    hello' "
  1540.  
  1541.     | re result lastre |
  1542.     lastre := 0.
  1543.     [ re := aString findString: 'Re:' startingAt: lastre + 1. lastre < re ] whileTrue: [ lastre := re ].
  1544.     lastre > 0
  1545.         ifTrue: [ result := aString copyFrom: lastre + 3 to: aString size ]
  1546.         ifFalse: [ result := aString copy ].
  1547.     ^result! !
  1548.  
  1549. !NewsSubject methodsFor: 'testing'!
  1550.  
  1551. isFromGroup: aNewsGroup
  1552.  
  1553.     ^aNewsGroup = newsgroup! !
  1554.  
  1555. !NewsSubject methodsFor: 'accessing'!
  1556.  
  1557. addMessage: aNewsMessage
  1558.  
  1559.     (messages includes: aNewsMessage) ifFalse: [ messages add: aNewsMessage ]!
  1560.  
  1561. messages
  1562.  
  1563.     ^messages!
  1564.  
  1565. removeMessage: aNewsMessage
  1566.  
  1567.     messages remove: aNewsMessage ifAbsent: [ nil ]!
  1568.  
  1569. removeReadMessages
  1570.  
  1571.     "Ask our newsgroup about each message -- remove if read"
  1572.  
  1573.     messages copy do: [ :aMessage |
  1574.         (newsgroup haveRead: aMessage) ifTrue: [ self removeMessage: aMessage ]]!
  1575.  
  1576. subjectString
  1577.  
  1578.     ^subjectString!
  1579.  
  1580. subjectStringWithOneRe
  1581.  
  1582.     ^'Re: ', (self stripReFrom: subjectString)! !
  1583.  
  1584. !NewsSubject methodsFor: 'printing'!
  1585.  
  1586. printString
  1587.  
  1588.     ^subjectString! !
  1589. "-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!
  1590.  
  1591. NewsSubject class
  1592.     instanceVariableNames: ''!
  1593.  
  1594.  
  1595. !NewsSubject class methodsFor: 'instance creation'!
  1596.  
  1597. newCalled: aString
  1598.  
  1599.     ^self new initializeWith: aString!
  1600.  
  1601. newWithString: aString inGroup: aNewsGroup
  1602.  
  1603.     ^self new initializeWithString: aString newsGroup: aNewsGroup! !
  1604.  
  1605. OrderedCollection variableSubclass: #SparseIntegerCollection
  1606.     instanceVariableNames: ''
  1607.     classVariableNames: ''
  1608.     poolDictionaries: ''
  1609.     category: 'Tools-NewsReader'!
  1610. SparseIntegerCollection comment:
  1611. 'This collection will hold a large number of Integers while minimizing the actual size of the collection.  It does this by storing intervals when possible.  For instance, an instance of this class can hold the integers 1 to 100, and 102.  It really contains two elements - the interval (1 to: 100), and 102.  If 101 is subsequently added, it will contain only one element - the interval (1 to: 102).'!
  1612.  
  1613.  
  1614. !SparseIntegerCollection methodsFor: 'initialization'!
  1615.  
  1616. initializeFromRnString: aString
  1617.  
  1618.     "Initialize this instance based on the .newsrc format supplied in aString"
  1619.  
  1620.     | stream first last sep |
  1621.     stream := ReadStream on: aString.
  1622.     [ stream skipSeparators atEnd ] whileFalse: [
  1623.         first := WriteStream on: String new.
  1624.         [ stream atEnd not and: [ stream peek isDigit ]] whileTrue: [ first nextPut: stream next ].
  1625.         (stream atEnd or: [ (sep := stream next) = $, ])
  1626.             ifTrue: [ "single number"
  1627.                 first contents = '' ifFalse: [ self add: first contents asNumber ]]
  1628.             ifFalse: [ "interval"
  1629.                 sep = $- ifFalse: [ ^self error: 'unknown format' ].
  1630.                 last := WriteStream on: String new.
  1631.                 [ stream atEnd not and: [ stream peek isDigit ]] whileTrue: [ last nextPut: stream next ].
  1632.                 self add: (first contents asNumber to: last contents asNumber) ]]! !
  1633.  
  1634. !SparseIntegerCollection methodsFor: 'accessing'!
  1635.  
  1636. collapse
  1637.  
  1638.     "Replace contents with a single interval from the lowest to the highest"
  1639.  
  1640.     self add: (self first to: self last)!
  1641.  
  1642. first
  1643.  
  1644.     | elem |
  1645.     self emptyCheck.
  1646.     elem := super first.
  1647.     (elem isKindOf: Interval)
  1648.         ifTrue: [ ^elem first ]
  1649.         ifFalse: [ ^elem ]!
  1650.  
  1651. last
  1652.  
  1653.     | elem |
  1654.     self emptyCheck.
  1655.     elem := super last.
  1656.     (elem isKindOf: Interval)
  1657.         ifTrue: [ ^elem last ]
  1658.         ifFalse: [ ^elem ]! !
  1659.  
  1660. !SparseIntegerCollection methodsFor: 'adding'!
  1661.  
  1662. add: anIntegerOrInterval
  1663.  
  1664.     "Dispatch to appropriate method, depending on class of argument"
  1665.     (anIntegerOrInterval isMemberOf: Interval)
  1666.         ifTrue: [ ^self addInterval: anIntegerOrInterval ].
  1667.     (anIntegerOrInterval isKindOf: Integer)
  1668.         ifTrue: [ ^self addInteger: anIntegerOrInterval ].
  1669.     ^self error: 'SparceIntegerCollection can only hold Integers or Intervals'!
  1670.  
  1671. addInteger: anInteger
  1672.  
  1673.     "For now, make it an interval"
  1674.     ^self addInterval: (anInteger to: anInteger)!
  1675.  
  1676. addInterval: anInterval
  1677.  
  1678.     | newStartPosition newStopPosition newInterval newStart newStop |
  1679.  
  1680.     "Interval must be of increment 1"
  1681.     (anInterval increment == 1) ifFalse: [ ^self error: 'Interval has increment too large for SparseIntegerCollection' ].
  1682.  
  1683.     "Determine the new interval's position"
  1684.     newStartPosition := self startPositionOf: anInterval first.
  1685.     newStopPosition := self stopPositionOf: anInterval last.
  1686.  
  1687.     "Maximize the new interval"
  1688.     (newStartPosition <= self size)
  1689.         ifTrue: [ newStart := anInterval first min: (self at: newStartPosition) first ]
  1690.         ifFalse: [ newStart := anInterval first ].
  1691.     (newStopPosition > 0)
  1692.         ifTrue: [ newStop := anInterval last max: (self at: newStopPosition) last ]
  1693.         ifFalse: [ newStop := anInterval last ].
  1694.     newInterval := newStart to: newStop.
  1695.  
  1696.     "Insert or replace with new interval"
  1697.     (newStartPosition > newStopPosition)
  1698.         ifTrue: [ "Insert"
  1699.             self add: newInterval beforeIndex: newStartPosition ]
  1700.         ifFalse: [ "Replace"
  1701.             self replaceFrom: newStartPosition to: newStopPosition withElement: newInterval ]! !
  1702.  
  1703. !SparseIntegerCollection methodsFor: 'removing'!
  1704.  
  1705. remove: anIntegerOrInterval ifAbsent: aBlock
  1706.  
  1707.     (self includes: anIntegerOrInterval)
  1708.         ifFalse: [ ^aBlock value ]
  1709.         ifTrue: [
  1710.             "Dispatch to appropriate method, depending on class of argument"
  1711.             (anIntegerOrInterval isMemberOf: Interval)
  1712.                 ifTrue: [ ^self removeInterval: anIntegerOrInterval ]
  1713.                 ifFalse: [ ^self removeInteger: anIntegerOrInterval ]]!
  1714.  
  1715. removeAll
  1716.  
  1717.     self removeAll: self! !
  1718.  
  1719. !SparseIntegerCollection methodsFor: 'testing'!
  1720.  
  1721. includes: anIntegerOrInterval
  1722.  
  1723.     "Dispatch to appropriate method, depending on class of argument"
  1724.     (anIntegerOrInterval isMemberOf: Interval)
  1725.         ifTrue: [ ^self includesInterval: anIntegerOrInterval ].
  1726.     (anIntegerOrInterval isKindOf: Integer)
  1727.         ifTrue: [ ^self includesInteger: anIntegerOrInterval ].
  1728.     ^false!
  1729.  
  1730. includesInteger: anInteger
  1731.  
  1732.     "Return true if I contain anInteger or any interval containing anInteger"
  1733.  
  1734.     self do: [ :anInterval |
  1735.         "(anInterval includes: anInteger)"
  1736.         ((anInteger >= anInterval first) and: [anInteger <= anInterval last]) ifTrue: [ ^true ]].
  1737.     ^false!
  1738.  
  1739. includesInterval: anInterval
  1740.  
  1741.     anInterval do: [ :anInt | (self includesInteger: anInt) ifFalse: [ ^false ]].
  1742.     ^true! !
  1743.  
  1744. !SparseIntegerCollection methodsFor: 'printing'!
  1745.  
  1746. storeRnStyleOn: aStream
  1747.  
  1748.     "Store using '-' and ','.  For example, if I represent the numbers 1 to 5 and 7, store myself '1-5,7'"
  1749.  
  1750.     | first |
  1751.     first := true.
  1752.     super do: [ :elem |
  1753.         first
  1754.             ifTrue: [ first := false ]
  1755.             ifFalse:  [aStream nextPut: $, ].
  1756.         (elem isKindOf: Interval)
  1757.             ifTrue: [
  1758.                 | start stop |
  1759.                 start := elem first.
  1760.                 stop := elem last.
  1761.                 start = stop
  1762.                     ifTrue: [ start storeOn: aStream ]
  1763.                     ifFalse: [
  1764.                         start storeOn: aStream.
  1765.                         aStream nextPut: $-.
  1766.                         stop storeOn: aStream ]]
  1767.             ifFalse: [
  1768.                 elem storeOn: aStream ]]! !
  1769.  
  1770. !SparseIntegerCollection methodsFor: 'private'!
  1771.  
  1772. removeInteger: anInteger
  1773.  
  1774.     "Private:  this method assumes anInteger exists in receiver"
  1775.  
  1776.     | index theInterval first last |
  1777.     index := self startPositionOf: anInteger.
  1778.     theInterval := self at: index.
  1779.     first := theInterval first.
  1780.     last := theInterval last.
  1781.     (first == last) ifTrue: [
  1782.         first == anInteger ifFalse: [ ^self error: 'internal error' ].
  1783.         self removeAtIndex: index.
  1784.         ^anInteger ].
  1785.     anInteger == first ifTrue: [
  1786.         self at: index put: (first + 1 to: last).
  1787.         ^anInteger ].
  1788.     anInteger == last ifTrue: [
  1789.         self at: index put: (first to: last - 1).
  1790.         ^anInteger ].
  1791.  
  1792.     "So, anInteger is in the middle of theInterval.  We must break
  1793.     theInterval into two intervals."
  1794.     self at: index put: (first to: anInteger - 1).
  1795.     self add: (anInteger + 1 to: last) beforeIndex: index + 1.
  1796.     ^anInteger!
  1797.  
  1798. removeInterval: anInterval
  1799.  
  1800.     "Private:  this method assumes anInterval exists in receiver"
  1801.  
  1802.     ^self error: 'not yet implemented'!
  1803.  
  1804. replaceFrom: start to: stop withElement: element
  1805.  
  1806.     "This is like replaceFrom:to:with:, except: 1) element is a single element to be added, not a collection; and 2), the distance from start to stop can be 1 or greater"
  1807.  
  1808.     (start to: stop) reverseDo: [ :index | self removeAtIndex: index ].
  1809.     self add: element beforeIndex: start!
  1810.  
  1811. startPositionOf: anInteger
  1812.  
  1813.     "((SparseIntegerCollection new: 2)
  1814.             addLast: (1 to: 5);
  1815.             addLast: (10 to: 15);
  1816.             yourself)
  1817.         startPositionOf: 6"
  1818.  
  1819.     1 to: self size do: [ :index |
  1820.         (self at: index) last + 1 < anInteger
  1821.             ifFalse: [ ^index ]].
  1822.     ^self size + 1!
  1823.  
  1824. stopPositionOf: anInteger
  1825.  
  1826.     "((SparseIntegerCollection new: 2)
  1827.             addLast: (3 to: 5);
  1828.             yourself)
  1829.         stopPositionOf: 7"
  1830.  
  1831.     1 to: self size do: [ :index |
  1832.         (self at: index) first - 1 > anInteger
  1833.             ifTrue: [ ^index - 1 ]].
  1834.     ^self size!
  1835.  
  1836. tests
  1837.  
  1838.     " | aSIC |
  1839.     aSIC := SparseIntegerCollection new.
  1840.     aSIC add: 3; add: 4; add: 5; add: 1.
  1841.     aSIC "
  1842.  
  1843.     " | aSIC |
  1844.     aSIC := SparseIntegerCollection new.
  1845.     aSIC add: 3; add: 4; add: 5; add: 7.
  1846.     aSIC "
  1847.  
  1848.     " | aSIC |
  1849.     aSIC := SparseIntegerCollection new.
  1850.     aSIC add: 1; add: 2; add: 6; add: 7; add: 4.
  1851.     aSIC "
  1852.  
  1853.     " | aSIC |
  1854.     aSIC := SparseIntegerCollection new.
  1855.     aSIC add: 1; add: 2; add: 10; add: 11; add: 5; add: 6; add: (4 to: 7).
  1856.     aSIC "
  1857.  
  1858.     " | aSIC |
  1859.     aSIC := SparseIntegerCollection new.
  1860.     aSIC add: 1; add: 2; add: 10; add: 11; add: 3.
  1861.     aSIC "
  1862.  
  1863.     " | aSIC |
  1864.     aSIC := SparseIntegerCollection new.
  1865.     aSIC add: 1; add: 2; add: 10; add: 11; add: 9.
  1866.     aSIC "
  1867.  
  1868.     " | aSIC |
  1869.     aSIC := SparseIntegerCollection new.
  1870.     aSIC add: 1; add: 2; add: 10; add: 11; add: 5; add: 6; add: (2 to: 10).
  1871.     aSIC "
  1872.  
  1873.     " | aSIC |
  1874.     aSIC := SparseIntegerCollection new.
  1875.     aSIC add: (2 to: 10); add: 3.
  1876.     aSIC "
  1877.  
  1878.     " | aSIC |
  1879.     aSIC := SparseIntegerCollection new.
  1880.     aSIC add: 1677; add: 1678; add: 1679; add: 1937.
  1881.     aSIC "! !
  1882. "-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!
  1883.  
  1884. SparseIntegerCollection class
  1885.     instanceVariableNames: ''!
  1886.  
  1887.  
  1888. !SparseIntegerCollection class methodsFor: 'instance creation'!
  1889.  
  1890. fromRnString: aString
  1891.  
  1892.     "Create an instance as described by aString (.newsrc format)"
  1893.  
  1894.     ^self new initializeFromRnString: aString! !
  1895. NewsGroup initialize!
  1896.  
  1897. NewsReader initialize!
  1898.  
  1899.  
  1900.